﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Reflection;
using System.CodeDom;

namespace Milo.Builder
{
    using Framework;

    /// <summary>
    /// Builder for an individual model class
    /// </summary>
    public class ClassBuilder
    {
		private Type type;

		public Type Type
		{
			get { return type; }
			set { type = value; }
		}

        public ClassBuilder(Type type)
        {
            this.type = type;
        }

        /// <summary>
        /// Gets all the class type referred to by the properties and field
        /// </summary>
        /// <returns></returns>
        public IList<Type> GetChildTypes()
        {
            List<Type> types = new List<Type>();

			foreach (PropertyInfo propertyInfo in Type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
			{
				Type propertyType = propertyInfo.PropertyType;
				if (propertyType.IsClass && !typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyType))
					types.Add(propertyType);
			}

			foreach (FieldInfo fieldInfo in Type.GetFields(BindingFlags.Instance | BindingFlags.Public))
			{
				Type fieldType = fieldInfo.FieldType;
				if (fieldType.IsClass && !typeof(System.Collections.IEnumerable).IsAssignableFrom(fieldType))
					types.Add(fieldInfo.FieldType);
			}

            return types;
        }

        public CodeTypeDeclaration GenerateClassCode()
        {
            CodeTypeDeclaration ctd = new CodeTypeDeclaration();
            ctd.IsClass = true;
            ctd.IsPartial = true;
            ctd.Name = Type.Name + "Control";

            CodeTypeReference ctr = new CodeTypeReference(typeof(ListControl<>));
            ctr.TypeArguments.Add(Type.Name);
            ctd.BaseTypes.Add(ctr);
            ctr = new CodeTypeReference(typeof(IListControl<>));
            ctr.TypeArguments.Add(Type.Name);
            ctd.BaseTypes.Add(ctr);

            //constructors next
			CodeConstructor ccc = new CodeConstructor();
			ccc.Attributes = MemberAttributes.Public;
			ctd.Members.Add(ccc);

			ccc = new CodeConstructor();
            ccc.Attributes = MemberAttributes.Public;
			ccc.Parameters.Add(new CodeParameterDeclarationExpression(Type.Name, "model"));
			ccc.BaseConstructorArgs.Add(new CodeSnippetExpression("model"));
			ctd.Members.Add(ccc);

            ccc = new CodeConstructor();
            ccc.Attributes = MemberAttributes.Public;
            ccc.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "name"));
            ccc.Parameters.Add(new CodeParameterDeclarationExpression(Type.Name, "model"));
            ccc.BaseConstructorArgs.Add(new CodeSnippetExpression("name"));
            ccc.BaseConstructorArgs.Add(new CodeSnippetExpression("model"));
            ctd.Members.Add(ccc);

            //do the properties and fields now
            foreach (PropertyInfo pi in Type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
                if (pi.CanRead) // a bit pointless otherwise...
                    GenerateProperty(ctd, pi.Name, pi.PropertyType, pi.CanWrite && pi.GetSetMethod() != null);

            foreach (FieldInfo fi in Type.GetFields(BindingFlags.Instance | BindingFlags.Public))
                GenerateProperty(ctd, fi.Name, fi.FieldType, true);

            return ctd;
        }

		private bool AssignableAsIList(Type type)
		{
			return type.IsArray ||
				(type.IsGenericType && typeof(IList<>).IsAssignableFrom(type.GetGenericTypeDefinition()));
		}

		public string GetControlClassFullname(Type type)
		{
			return string.Format("{0}.Controls.{1}Control", type.Namespace, type.Name);
		}

        private void GenerateProperty(CodeTypeDeclaration ctd, string propertyName, Type type, bool canWrite)
        {
            string privatePropertyName = "_" + propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1);
            string getterName = "get_" + propertyName + "_value";
            string setterName = "set_" + propertyName + "_value";

			//is this an ilist?
			bool useIList = AssignableAsIList(type);

			CodeTypeReference getterSetterType = new CodeTypeReference(type);


			if (useIList)
				//if it's an ilist the real type is the first generic parameter
				type = type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0];

			//TODO: don't do collections or enumerables cos they can't be indexed for binding 
			//(changed my mind, could use them for display only)

            bool useClass = (type.IsClass && !type.Equals(typeof(string)));

			CodeTypeReference propertyType;
			CodeTypeReference controlType;

			if (!useIList)
			{
				if (useClass)
				{
					propertyType = new CodeTypeReference(GetControlClassFullname(type));
					controlType = propertyType;
				}
				else
				{
					propertyType = new CodeTypeReference(typeof(IFieldControl<>));
					propertyType.TypeArguments.Add(type);
					controlType = new CodeTypeReference(typeof(FieldControl<>));
					controlType.TypeArguments.Add(type);					
				}
			}
			else
			{
				propertyType = new CodeTypeReference(typeof(IControlList<,>));
				propertyType.TypeArguments.Add(type);
				controlType = new CodeTypeReference(typeof(ControlList<,>));
				controlType.TypeArguments.Add(type);

				CodeTypeReference modelType;

				if (useClass)
					modelType = new CodeTypeReference(GetControlClassFullname(type));
				else
				{
					modelType = new CodeTypeReference(typeof(FieldControl<>));
					modelType.TypeArguments.Add(type);
				}

				propertyType.TypeArguments.Add(modelType);
				controlType.TypeArguments.Add(modelType);
			}

            //private field for control
			CodeMemberField cmf = new CodeMemberField(propertyType, privatePropertyName);
            ctd.Members.Add(cmf);

            //getter and setters
			//if (useIList)
			//{
			//    getterSetterType = new CodeTypeReference(typeof(IList<>));
			//    getterSetterType.TypeArguments.Add(type);
			//}
			//else
			//    getterSetterType = new CodeTypeReference(type);

            CodeMemberMethod cmm = new CodeMemberMethod();
            cmm.Attributes = MemberAttributes.Private;
            cmm.Name = getterName;
			cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(int), "index"));
			cmm.ReturnType = getterSetterType;
            cmm.Statements.Add(new CodeMethodReturnStatement(new CodeArgumentReferenceExpression("Value." + propertyName)));
            ctd.Members.Add(cmm);

            if (canWrite)
            {
                cmm = new CodeMemberMethod();
                cmm.Attributes = MemberAttributes.Private;
                cmm.Name = setterName;
				cmm.Parameters.Add(new CodeParameterDeclarationExpression(getterSetterType, "value"));
				cmm.Parameters.Add(new CodeParameterDeclarationExpression(typeof(int), "index"));
				cmm.Statements.Add(new CodeAssignStatement(new CodeArgumentReferenceExpression("Value." + propertyName), new CodeArgumentReferenceExpression("value")));
                ctd.Members.Add(cmm);
            }

            //property
            //ctr = new CodeTypeReference();
            //ctr.BaseType = useClass ? "IListControl" : "IFieldControl";
            //ctr.TypeArguments.Add(type);

            CodeMemberProperty cmp = new CodeMemberProperty();
            cmp.Attributes = MemberAttributes.Public; //TODO: stop being marked as virtual
            cmp.Type = propertyType;

			//Tweak to avoid name clash
			if (propertyName == "Name" || propertyName == "Value")
				cmp.Name = "Model" + propertyName;
			else
				cmp.Name = propertyName;

            cmp.GetStatements.Add(new CodeConditionStatement(new CodeBinaryOperatorExpression(new CodeVariableReferenceExpression(privatePropertyName),
                CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression(null)),
                new CodeStatement[]{new CodeAssignStatement(new CodeVariableReferenceExpression(privatePropertyName),
                    new CodeObjectCreateExpression(controlType, new CodeExpression[]{})),
					new CodeExpressionStatement(
					new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeVariableReferenceExpression(privatePropertyName),"LinkToParent"),
                        new CodeExpression[]
                        {
                            new CodePrimitiveExpression(propertyName),
                            new CodeThisReferenceExpression(),
                            new CodeVariableReferenceExpression(getterName),
                            (canWrite?(CodeExpression)new CodeVariableReferenceExpression(setterName):(CodeExpression)new CodePrimitiveExpression(null)),
							new CodePrimitiveExpression(0)
                        }
						))}));
            cmp.GetStatements.Add(new CodeMethodReturnStatement(new CodeArgumentReferenceExpression(privatePropertyName)));
            ctd.Members.Add(cmp);
        }
    }
}